Chapter 2

序列组成的数组

2.1 序列分类

按存放数据的类型是否一致,序列分为容器序列扁平序列
容器序列存放数据的类型可以不同,如:list, tuple, collections.deque
扁平序列存放灵气的类型必需一致,如:str, bytes, bytearray, memoryview, array.array

另外,序列数型也可以按能否被修改,将序列分为:可变序列(MutableSequence)不可变序列(Sequence)
除了字面意思的可变和不可变的不同外,最根本的区别是继承的父类不同。可变序列继承自不可变序列(即不可变序列是可变序列的父类)。

2.2 列表推导和生成器表达式

列表推导(list comprehension, listcomps 是构建列表(list)的快捷方式,而 生成器表达式(generator expression, genexps 则可以用来创建其他任何类型的序列。

2.2.1 列表推导的可读性

pass  # 列表推导  
  
symbols = '$¢£¥€¤'  
# for 循环创建列表  
codes = []  
for symbol in symbols:  
    codes.append(ord(symbol))  
  
# print(codes)  
  
# 列表推导创建列表  
codes_ = [ord(symbol) for symbol in symbols]  
# print(codes_)  

列表推导使用准则:

  1. 列表推导的代码超过两行,则考虑使用 for 循环;
  2. 列表推导只能生成列表。如果想生成其他类型的序列,则需要用到生成器表达式。

2.2.2 列表推导同 filtermap 的比较

# 例 2-3:超越 ASCII 的数字  
# 列表推导完成  
symbols = '$¢£¥€¤'
beyond_ascii = [ord(s) for s in symbols if ord(s) > 127]  
# print(beyond_ascii)  
# map, filter 组合完成  
beyond_ascii = list(filter(lambda c: c > 127, map(ord, symbols)))  
# print(beyond_ascii)

2.2.3 笛卡尔积

# 列表推导计算笛卡尔积  
# 例 2-4:3 种不同尺寸的 T 恤衫,每个尺寸都有 2 个颜色,将 6 种不同的组合放在列表中  
# 列表推导完成  
colors = ["black", "white"]  
sizes = ["S", "M", "L"]  
# tshirts = [(color, size) for color in colors for size in sizes]  # flag_1  
tshirts = [(color, size) for size in sizes for color in colors]  # flag_2  

# print(tshirts)  

# for 循环完成  
tshirts = []  
for color in colors:  
    for size in sizes:  
        tshirts.append((color, size))  
# print(tshirts)

注释 flag_1flag_2for 循环中 size, color 的位置不同,tshirt 的分组方式不同。
flag_1 中, color 是外循环,size 是内循环, 以 color 分组;
flag_2 中, size 是外循环,color 是内循环, 以 size 分组。

flag_1 输出结果:

('black', 'S')
('black', 'M')
('black', 'L')
('white', 'S')
('white', 'M')
('white', 'L')

flag_2 输出结果:

('black', 'S')
('white', 'S')
('black', 'M')
('white', 'M')
('black', 'L')
('white', 'L')

2.2.4 生成器表达式

生成器表达式列表推导 一样,只是将 列表推导中方括号 改成 圆括号

# 例 2-5
tuple_ = tuple(ord(symbol) for symbol in symbols)  #  one
# print(tuple_)  
array_ = array.array("I", (ord(symbol) for symbol in symbols))  # two
# print(array_)

生成器表达式 是一个函数的唯一参数时,不需要用括号括起来,如注释 one
array 函数有两个参数,此时 生成器表达式 需要用括号括起来,如注释 two

# 例 2-6:生成器表达式计算笛卡尔积  
tshirts = ("%s %s" % (c, s) for c in colors for s in sizes)  
print(tshirts)  # 输出 tshirts 的类型和地址,<generator object <genexpr> at 0x000002597C146820>
for tshirt in tshirts:  
    print(tshirt)

2.3 元组

元组的特点:

  1. 元组为不可变列表;
  2. 元组可以用于没有字段的记录。

2.3.1 元组拆包

把元组中的所有元素分别赋值到不同的变量,称为元组拆包。元组拆包可以应用到任何可以迭代的对象上。元组中有 N 个元素,则需要有 N 个变量。

# 元组拆包形式之平行赋值  
lax_coordinates = (33.9425, -118.408056)  
latitude, longitude = lax_coordinates  # 元组拆包  
print(latitude)  
print(longitude)

平行赋值:把一个可迭代对象里的元素,一并赋值到由对应的变量组成的元组中。

平行赋值可以不使用中间变量交换两个变量的值

# 不用中间变量交换两个变量的值  
a = 1  
b = 2  
a, b = b, a  
# print(a)  
# print(b)

*_(下划线) 的使用

# * 号运算符把一个可迭代对象拆开作为函数的参数  
s = divmod(20, 8)  # divmod 模运算  
print(s)  
t = (20, 8)  
s1 = divmod(*t)  #  
print(s1)  
quotient, remainder = divmod(*t)  
print(quotient)  
print(remainder)  
  
# os.path.split() 返回 (path, filename)
_, filename = os.path.split("/home/breky/.ssh/idrsa.pub")  
print(filename)  
"""  
元组拆包后,不需要的元素可以用占位符(_)代替。上例  
用 * 号处理剩下的元素。下例  
"""  
aa, bb, *rest = range(5)  
print(aa, bb, rest)  
aaa, *rest1, bbb = range(5)  # * 号可以用在任意位置,但只能出现一次  
print(aaa, rest1, bbb)  
  
metro_areas = [  
    ('Tokyo','JP',36.933,(35.689722,139.691667)),  
    ('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),  
    ('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),  
    ('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),  
    ('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)),  
]  
print('{:15} | {:^9} | {:^9}'.format('city', 'lat.', 'long.'))  
  
fmt = '{:15} | {:9.4f} | {:9.4f}'  
for name, cc, pop, (latitude, longitude) in metro_areas:  
    print(fmt.format(name, latitude, longitude))

2.3.2 具名元组

顾名思义,带名的元组称为具名元组

collections.namedtuple 可以用来构建一个带名的元组和一个有名字的类。

from collections import namedtuple
City = namedtuple("City", "name country population coordinates")  # 1
tokyo = City("Tokyo", "JP", 36.933, (35.689722, 139.1667))  # 2

# 以下为输出语名,需在终端中运行。如需在 IDE 中运行,需要使用 print。
tokyo
tokyo.population  # 3
tokyo.coordinates
tokyo[1]

# 1: 具名元组创建语法。创建一个具名元组需要两个参数,一个是类名,另一个是类的各个字段(类的属性)的名字。各个字段可以是由数个字符串组成的可迭代对象,也可以是由空格分隔开的各字段名组成的字符串。

# 2: 对具名元组(类)City 的各字段名(属性)赋值。数据要以一串参数的形式传入到构造函数中,只接受单一的可迭代的对象。

Think  One  Think:

# 2 中的 City# 1 中赋值语句(=)左边的 City 还是右边的 City

Test:

from collections import namedtuple
Person = namedtuple("People", "name age sex")  
Jason = Person("Jason", 18, "M")
Jason  # 输出 People(name='Jason', age=18, sex='M')
Jason.name  # 输出 'Jason'
People  # 输出 NameError: name 'People' is not defined
People.name  # 输出 NameError: name 'People' is not defined
Peterson = People("Peterson", 21, "M")  # 输出 NameError: name 'People' is not defined

Conclusion:

通过实验可以看出:具名元组的定义语句中 (Person = namedtuple("People", "name age sex")),赋值语句(=)左侧的变量名(Person)为定义具名元组时的类名,右侧的第一个参数(People)不知道是个什么东西,应该是元组(("name", "age", "sex"))的名,可能通常这两个值(赋值语句左侧的变量名和右侧的第一个参数)应该相同。

# 3: 可以通过字段名(tokyo.population)或者位置(tokyo[1])来获取一个字段的信息。

具名元组常的其他属性和方法

City._fields  # 1

LatLong = namedtuple("LatLong", "lat long")
delhi_data = ("Delhi NCR", "IN", 21.935, LatLong(28.613889, 77.208889))
delhi = City._make(delhi_data)  # 2 
delhi._asdict()  # 3
for  key, value in delhi._asdict().items():
	print(key + ":", value)

# 1: _fields 属性是一个包含这个类所有字段名称的元组

# 2: _make() 接受一个可迭代对象来生成一个实例,与 City(*delhi_data) 一样

# 3: _asdict() 把元组名以 collections.OrderedDict 的形式返回。可以利用它来把元组里的信息友好的显示出来。

2.4 切片

高级切片形式的用法

2.4.1 切片和区间忽略最后一个元素

切片和区间操作不包含区间范围的最后一个元素,其他语言中称之为左闭右开。这样的如处是:

  1. 当只有最后一个位置信息时,可以快速看出切片和区间里有几个元素,如 range(3)my_list[:3] 都返回 3 个元素。
  2. 当起止位置信息都可见时,可以快速计算出切片和区间的长度,用后一个数减去第一个下标(stop - start)即可。
  3. 可以利用凭意一个下标来把序列分割成不重叠的两部分,只要写成 my_list[:x]my_list[x:] 就可以了
l = [10, 20, 30, 40, 50, 60]
l[:2]  # 在下标 2 的地方分割,输出 [10, 20]
l[2:]  # 输出 [30, 40. 50, 60]
l[:3]  # 在下标 3 的地方分割,输出 [10, 20, 30]
l[3:]  # 输出 [40, 50, 60]

2.4.2 对象切片

s[a:b:c] 的意思是对 sab 之间以 c 为间隔取值。c 值也可以为负值,负值意味着反向取值。

s = 'bicycle'
s[::3]  # 输出 'bye'
s[::-1]  # 输出 'elcycib'
s[::-2]  # 输出 'eccb'

例 2-4-1: 纯文本文件形式的收据,以一行字符串的形式解析

invoice = """
... 0.....6................................40........52...55........
... 1909  Pimoroni PiBrella                    $17.50    3    $52.50
... 1489  6mm Tactile Switch x20                $4.95    2     $9.90
... 1510  Panavise Jr. - PV-201                $28.00    1    $28.00
... 1601  PiTFT Mini Kit 320x240               $34.95    1    $34.95
... """

SKU = slice(0, 6)
DESCRIPTION = slice(6, 40)
UNIT_PRICE = slice(40, 52)
QUANTITY = slice(52, 55)
ITEM_TOTAL = slice(55, None)
line_items = invoice.split('\n')[2:]

for item in line_items:
	print(item[UNIT_PRICE], item[DESCRIPTION])

# 输出
#    $17.5 09  Pimoroni PiBrella             
#     $4.9 89  6mm Tactile Switch x20        
#    $28.0 10  Panavise Jr. - PV-201         
#    $34.9 01  PiTFT Mini Kit 320x240 

2.4.3 多维切片和省略

[] 运算符里可以使用逗号分开的多个索引或者是切片。

2.4.4 给切片赋值

把切片放在赋值语句的左边,或把它作为 del 操作的对象,就可以对序列进行嫁接、切除或修改操作。

l = list(range(10))
l  # 输出 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]  # 1
l[2:5] = [20, 30]  # 2
l  # 输出 [0, 1, 20, 30, 5, 6, 7, 8, 9]  # 3
del l[5:7]  # 4
l  # 输出 [0, 1, 20, 30, 5, 8, 9]  # 5
l[3::2] = [11, 22] # 6
l  # 输出 [0, 1, 20, 11, 5, 22, 9] # 7
l[2:5] = 100  #  输出以下内容  # 8

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only assign an iterable

l[2:5] = [100]  # 9
l  # 输出 [0, 1, 100, 22, 9]  # 10
  1. 经过 # 2 的切片赋值操作,# 3 的输出结果与 # 1 的结果对比,除了下标 23 的值不两同外,l 的长度也不同。 分析一下 # 2 操作,赋值符号左边 l[2:5] 取的是 下标 234 的值,一共 3 个值,赋值符号右边 [20, 30] 是两个值,所以 # 2 的操作把 l 列表中下标为 23 的值分别改为 2030,下标为 4 的值直接删除,后边的值顺序前移,整个 l 的长度减 1
  2. # 8 操作报错原因。# 8 取出的是 l 的子序列,共 3 个值,不能用整数赋值。应该用相同的序列赋值,如 # 9

2.5 序列操作之 +*

Python+ 操作两侧的序列相同,在拼接过程中不会被修改,而是会新建一个序列来作为拼接结果,* 操作也一样。

l = [1, 2, 3]
l + [1, 2, 3]  # 输出:[1, 2, 3, 1, 2, 3]
l  # 输出: [1, 2, 3]
l * 5  # 输出: [1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
l  # 输出:[1, 2, 3]

如上代码段,+* 都遵循这个规律,不修改原有的操作对像,而是构建一个全新的序列。

需要注意的是:
使用 * 号对二维列表初始化时,产生二维列表中的 n 个一维列表,是指向了同一对象的引用的列表。即更改这 n 个一维列表中的某个值,其他 n-1 个一维列表中对应的值也会一起改为相同的值。如下代码段显示。

weird_board = [["_"] * 3] * 3
weird_board # 输出: [['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
weird_board[1][2] = "O"
weird_board  # 输出:[['_', '_', 'O'], ['_', '_', 'O'], ['_', '_', 'O']]

如上代码段,本意是想将 weird_board 的第二行第三列的元素改为 Oweird_board[1][2] = "O"),但实际输出却是所有行的第三个元素都改为了 O[['_', '_', 'O'], ['_', '_', 'O'], ['_', '_', 'O']])。可以使用列表推导的方式始初化二维列表中的一维列表,来达到每行列表均不相同的目的。如下代码段显示。

board = [["_"] * 3 for i in range(3)]
board  # 输出: [['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
board[1][2] = "X"
board  # 输出: [['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]

2.6 序列操作之增量赋值

增量赋值运算符:+=*=

+=:调用序列的 __iadd__(就地加法),如果没有则调用 __add__

a += b

上述代码,如果序列 a__iadd__ 方法,则调用。同时将结果赋值给 a ,与 a.extend(b) 一样。如果序列 a 没有 __iadd__ 方法,a += b 就变成跟 a = a + b 一 样。但是,不可变序列不支持这个操作。

l = [1, 2, 3]
id(l)  # 输出 l 的存储地址:2159542330752  # 1
l *= 2
l  # 输出: [1, 2, 3, 1, 2, 3]
id(l)  # 输出 l 的存储地址:2159542330752  # 2

t = (1, 2, 3)
id(t)  # 输出 t 的存储地址:2159542259392  # 3
t  # 输出:(1, 2, 3)
t *= 2  # 输出 t 的存储地址: 2159541382976 # 4
id(t) 
t  # 输出:(1, 2, 3, 1, 2, 3)

可变序列 l*= 操作前后,l 的值不同,对比 # 1# 2 的输出结果发现两个值相等,说明是同一变量。
不可变序列 t#= 操作前后, 虽然 t 的值不同,但是,对比 # 3# 4 的输出结果发现两个值不相等,说明不是同一变量,前一个 t 被后一个 t 覆盖,丢失。因为每次的拼接操作都会有一个新对象产生,所以效率很低。

2.7 list.sort 方法和内置函数 sorted

区别:

  1. list.sort 方法就地排序列表,即在原列表中排序。
  2. sorted 内置函数,会新建一个列表作为返回值,需要另一个变量存放,原表不发生改变。

相同之处: 两者都有两个可选关键字参数:reversekey

  1. reverseTrue,降序排列;False,升序排列。
  2. key:一个只有一个参数的函数,这个函数会被用在序列里的每一个元素上,所产生的结果将是排序算法依赖的对比关键字。如:key=str.lower,排序时忽略大小写;key=len,按字符串长度排序。默认是恒等函数(identity function),即按元素本身排序。
fruits = ['grape', 'raspberry', 'apple', 'banana']
sorted(fruits)  # 输出:['apple', 'banana', 'grape', 'raspberry']  
fruits  # 输出:['grape', 'raspberry', 'apple', 'banana']  
sorted(fruits, reverse=True)  # 输出:['raspberry', 'grape', 'banana', 'apple']  
sorted(fruits, key=len)  # 输出:['grape', 'apple', 'banana', 'raspberry']  
sorted(fruits, key=len, reverse=True)  # 输出:['raspberry', 'banana', 'grape', 'apple']  
fruits  # 输出:['grape', 'raspberry', 'apple', 'banana']  
fruits.sort()                          
fruits  # 输出:['apple', 'banana', 'grape', 'raspberry']